/****************************************************************************
 * Copyright (c) 2010 Torkild U. Resheim
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Torkild U. Resheim  - initial API and implementation
 *****************************************************************************/
package no.resheim.buildmonitor.hudson;

import java.io.NotSerializableException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Map;

import no.resheim.buildmonitor.core.BuildmonitorCore;
import no.resheim.buildmonitor.core.data.Server;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.ecf.core.IContainer;
import org.eclipse.ecf.core.IContainerListener;
import org.eclipse.ecf.core.events.ContainerConnectedEvent;
import org.eclipse.ecf.core.events.ContainerDisconnectingEvent;
import org.eclipse.ecf.core.events.IContainerEvent;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.identity.IDCreateException;
import org.eclipse.ecf.core.identity.IDFactory;
import org.eclipse.ecf.core.security.ConnectContextFactory;
import org.eclipse.ecf.core.security.IConnectContext;
import org.eclipse.ecf.remoteservice.IRemoteCall;
import org.eclipse.ecf.remoteservice.IRemoteService;
import org.eclipse.ecf.remoteservice.IRemoteServiceRegistration;
import org.eclipse.ecf.remoteservice.client.IRemoteCallParameter;
import org.eclipse.ecf.remoteservice.client.IRemoteCallable;
import org.eclipse.ecf.remoteservice.client.IRemoteResponseDeserializer;
import org.eclipse.ecf.remoteservice.client.IRemoteServiceClientContainerAdapter;
import org.eclipse.ecf.remoteservice.client.RemoteCallParameterFactory;
import org.eclipse.ecf.remoteservice.rest.RestCallFactory;
import org.eclipse.ecf.remoteservice.rest.RestCallableFactory;
import org.eclipse.ecf.ui.actions.AsynchContainerConnectAction;
import org.eclipse.ecf.ui.dialogs.IDCreateErrorDialog;
import org.eclipse.equinox.security.storage.StorageException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Variant of {@link Server} that supports connection to a <a
 * href="http://hudson-ci.org/">Hudson</a> continuous build server
 * 
 * @author Torkild Ulvøy Resheim
 * @since 1.0
 */
public class HudsonServer extends Server implements IContainerListener {

	/**
	 * Connects to the server every fifteen seconds in order to obtain a list of
	 * all active jobs.
	 * 
	 * @author Torkild Ulvøy Resheim
	 */
	class AutoRefreshThread extends Thread {

		boolean done = false;
		Object lock = new Object();

		public AutoRefreshThread() {
			setName(MessageFormat.format("Hudson server at {0} update",
					new Object[] { HudsonServer.this.getName() }));
			setDaemon(true);
		}

		public void finish() {
			synchronized (lock) {
				this.done = true;
				lock.notify();
			}
		}

		@Override
		public void run() {
			synchronized (lock) {
				while (!done) {
					try {
						lock.wait(getPollInterval());
						if (!done) {
							updateJobs();
						}
					} catch (final InterruptedException e) {
						// ignored
					}
				}
			}
		}
	}

	private AutoRefreshThread autoRefreshThread;

	IContainer container;

	IRemoteServiceRegistration registration;

	private IRemoteService restClientService;

	private IConnectContext context;

	public HudsonServer(IContainer container, URI uri) {
		super(container.getID().getName(), uri);
		this.container = container;
		initialize();
		container.addListener(this);
	}

	public void initialize() {
		IRemoteServiceClientContainerAdapter adapter = (IRemoteServiceClientContainerAdapter) container
				.getAdapter(IRemoteServiceClientContainerAdapter.class);
		// Setup authentication info
		try {
			URI root = new URI(container.getID().getName());
			context = ConnectContextFactory
					.createUsernamePasswordConnectContext(BuildmonitorCore
							.getDefault().getUsername(root), BuildmonitorCore
							.getDefault().getPassword(root));
			adapter.setConnectContextForAuthentication(context);
		} catch (URISyntaxException e) {
			e.printStackTrace();
		} catch (StorageException e) {
			e.printStackTrace();
		}
		// Setup resource handler
		adapter.setResponseDeserializer(createRestResource());
		// Callable for obtaining a list of all jobs. We want a list of all jobs
		// so we set the "depth" to 1.
		IRemoteCallParameter[] queryDepth = RemoteCallParameterFactory
				.createParameters("depth", "1");
		IRemoteCallable getJobsQuery = RestCallableFactory.createCallable(
				"getJobs", "/api/json", queryDepth);

		registration = adapter.registerCallables(
				new String[] { IHudsonQuery.class.getName() },
				new IRemoteCallable[][] { { getJobsQuery } }, null);

		restClientService = adapter.getRemoteService(registration
				.getReference());

	}

	/**
	 * Creates an instance of the type that is used to deserialise the objects
	 * we get from the REST service.
	 * 
	 * @return a response deserialiser
	 */
	private IRemoteResponseDeserializer createRestResource() {
		return new IRemoteResponseDeserializer() {

			@SuppressWarnings({ "rawtypes" })
			@Override
			public Object deserializeResponse(String endpoint,
					IRemoteCall call, IRemoteCallable callable,
					Map responseHeaders, String responseBody)
					throws NotSerializableException {
				try {
					// No need to continue on these as we won't get any data.
					if (call.getMethod().equals(".tempAction")) {
						return null;
					}
					if (container.getConnectedID() == null) {
						return null;
					}
					if (call.getMethod().equals(
							IHudsonQuery.class.getName() + ".getJobs")) {
						JSONObject node = new JSONObject(responseBody);
						// System.out.println(node.getJSONArray("assignedLabels")
						// .toString(2));
						decodeLabels(node.getJSONArray("assignedLabels"));
						JSONArray jobs = node.getJSONArray("jobs");
						// Iterate through the entire list of jobs. This
						// operation is rather lengthy.
						for (int x = 0; x < jobs.length(); x++) {
							// Connection has been cancelled so we're not going
							// to continue iterating through the jobs.
							if (container.getConnectedID() == null) {
								return null;
							}
							JSONObject serializedJob = jobs.getJSONObject(x);
							HudsonJob job = null;
							// The job is already in our list
							URI uri = new URI(serializedJob.getString("url"));
							if (hasJob(uri)) {
								job = (HudsonJob) getJob(uri);
								job.decode(serializedJob);
								if (job.hasChanged()) {
									// If the job has changed, so have this
									// instance.
									setChanged();
									notifyObservers(job);
									// Something may be listening to the job
									// itself.
									job.notifyObservers();
								}
							} else {
								job = new HudsonJob(container, serializedJob);
								// We know we're going to need this
								job.getLastStartedBuild();
								addJob(job);
							}
						}
						// TODO: Remove jobs that are no longer present.
					} else if (call.getMethod().equals(
							IHudsonQuery.class.getName() + ".buildQuery")) {
						return new JSONObject(responseBody);
					} else if (call.getMethod().equals(
							IHudsonQuery.class.getName() + ".getLogAction")) {
						return responseBody;
					}
				} catch (JSONException e) {
					throw new NotSerializableException(
							"JSON array parse exception: " + e.getMessage());
				} catch (URISyntaxException e) {
					e.printStackTrace();
				} catch (NumberFormatException e) {
					e.printStackTrace();
				}
				return null;
			}
		};
	}

	private void decodeLabels(JSONArray labels) throws JSONException {
		JSONObject root = labels.getJSONObject(0);
		setDescription(root.getString("description"));
	}

	@Override
	public void handleEvent(IContainerEvent event) {
		if (event instanceof ContainerConnectedEvent) {
			updateJobs();
			// Do not start the automatic refresh thread if the connection was
			// cancelled just after being connected.
			if (container.getConnectedID() != null) {
				startAutoRefresher();
			}
		}
		if (event instanceof ContainerDisconnectingEvent) {
			stopAutoRefresher();
		}
	}

	/**
	 * Start the automatic refresher. When running it will connect to the Hudson
	 * service and obtain information about it's jobs.
	 * 
	 * @see #stopAutoRefresher()
	 * @see #updateJobs()
	 */
	private void startAutoRefresher() {
		if (autoRefreshThread == null) {
			System.out.println("HudsonServer.startAutoRefresher()");
			autoRefreshThread = new AutoRefreshThread();
			autoRefreshThread.start();
		}
	}

	/**
	 * Stop the automatic refresher.
	 * 
	 * @see #startAutoRefresher()
	 */
	private void stopAutoRefresher() {
		if (autoRefreshThread != null) {
			System.out.println("HudsonServer.stopAutoRefresher()");
			autoRefreshThread.finish();
			autoRefreshThread = null;
		}
	}

	/**
	 * Updates the list of jobs.
	 */
	private void updateJobs() {
		// Clear the status
		setStatus(Status.OK_STATUS);
		try {
			restClientService.callSync(RestCallFactory
					.createRestCall(IHudsonQuery.class.getName() + ".getJobs"));
		} catch (Exception e) {
			setStatus(new Status(IStatus.ERROR, BuildmonitorCore.PLUGIN_ID,
					"Could not update jobs", e));
			notifyObservers();
		}
	}

	@Override
	public IStatus disconnect() {
		System.out.println("Shutting down");
		container.disconnect();
		return Status.OK_STATUS;
	}

	@Override
	public IStatus connect() {
		ID targetID = null;
		try {
			targetID = IDFactory.getDefault().createID(
					container.getConnectNamespace(), getUri().toString());
		} catch (final IDCreateException e) {
			new IDCreateErrorDialog(null, getUri().toString(), e).open();
		}
		new AsynchContainerConnectAction(container, targetID, context, null)
				.run();
		return Status.OK_STATUS;
	}
}
